/*
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a
* copy of the License at the following location:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jasig.cas.adaptors.x509.authentication.handler.support;
import java.security.GeneralSecurityException;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.Set;
import java.util.regex.Pattern;
import org.jasig.cas.adaptors.x509.authentication.principal.X509CertificateCredential;
import org.jasig.cas.adaptors.x509.util.CertUtils;
import org.jasig.cas.authentication.HandlerResult;
import org.jasig.cas.authentication.PreventedException;
import org.jasig.cas.authentication.handler.support.AbstractPreAndPostProcessingAuthenticationHandler;
import org.jasig.cas.authentication.Credential;
import org.jasig.cas.authentication.principal.SimplePrincipal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.login.FailedLoginException;
import javax.validation.constraints.NotNull;
/**
* Authentication Handler that accepts X509 Certificates, determines their
* validity and ensures that they were issued by a trusted issuer. (targeted at
* X509v3) Optionally checks KeyUsage extension in the user certificate
* (container should do that too). Note that this handler trusts the servlet
* container to do some initial checks like path validation. Deployers can
* supply an optional pattern to match subject dns against to further restrict
* certificates in case they are not using their own issuer. It's also possible
* to specify a maximum pathLength for the SUPPLIED certificates. (note that
* this does not include a pathLength check for the root certificate)
* [PathLength is 0 for the CA certificate that issues the end-user certificate]
*
* @author Scott Battaglia
* @author Jan Van der Velpen
* @since 3.0.4
*/
public class X509CredentialsAuthenticationHandler extends AbstractPreAndPostProcessingAuthenticationHandler {
/** Default setting to limit the number of intermediate certificates. */
private static final int DEFAULT_MAXPATHLENGTH = 1;
/** Default setting whether to allow unspecified number of intermediate certificates. */
private static final boolean DEFAULT_MAXPATHLENGTH_ALLOW_UNSPECIFIED = false;
/** Default setting to check keyUsage extension. */
private static final boolean DEFAULT_CHECK_KEYUSAGE = false;
/** Default setting to force require "KeyUsage" extension. */
private static final boolean DEFAULT_REQUIRE_KEYUSAGE = false;
/** Default subject pattern match. */
private static final Pattern DEFAULT_SUBJECT_DN_PATTERN = Pattern.compile(".*");
/** OID for KeyUsage X.509v3 extension field. */
private static final String KEY_USAGE_OID = "2.5.29.15";
/** Instance of Logging. */
private final Logger logger = LoggerFactory.getLogger(getClass());
/** The compiled pattern supplied by the deployer. */
@NotNull
private Pattern regExTrustedIssuerDnPattern;
/**
* Deployer supplied setting for maximum pathLength in a SUPPLIED
* certificate.
*/
private int maxPathLength = DEFAULT_MAXPATHLENGTH;
/**
* Deployer supplied setting to allow unlimited pathLength in a SUPPLIED
* certificate.
*/
private boolean maxPathLengthAllowUnspecified = DEFAULT_MAXPATHLENGTH_ALLOW_UNSPECIFIED;
/** Deployer supplied setting to check the KeyUsage extension. */
private boolean checkKeyUsage = DEFAULT_CHECK_KEYUSAGE;
/**
* Deployer supplied setting to force require the correct KeyUsage
* extension.
*/
private boolean requireKeyUsage = DEFAULT_REQUIRE_KEYUSAGE;
/** The compiled pattern for trusted DN's supplied by the deployer. */
@NotNull
private Pattern regExSubjectDnPattern = DEFAULT_SUBJECT_DN_PATTERN;
/** Certificate revocation checker component. */
@NotNull
private RevocationChecker revocationChecker = new NoOpRevocationChecker();
@Override
public boolean supports(final Credential credential) {
return credential != null
&& X509CertificateCredential.class.isAssignableFrom(credential
.getClass());
}
/** {@inheritDoc} */
@Override
protected final HandlerResult doAuthentication(final Credential credential) throws GeneralSecurityException, PreventedException {
final X509CertificateCredential x509Credential = (X509CertificateCredential) credential;
final X509Certificate[] certificates = x509Credential.getCertificates();
X509Certificate clientCert = null;
boolean hasTrustedIssuer = false;
for (int i = certificates.length - 1; i >= 0; i--) {
final X509Certificate certificate = certificates[i];
logger.debug("Evaluating {}", CertUtils.toString(certificate));
validate(certificate);
if (!hasTrustedIssuer) {
hasTrustedIssuer = isCertificateFromTrustedIssuer(certificate);
}
// getBasicConstraints returns pathLenContraint which is generally
// >=0 when this is a CA cert and -1 when it's not
int pathLength = certificate.getBasicConstraints();
if (pathLength < 0) {
logger.debug("Found valid client certificate");
clientCert = certificate;
} else {
logger.debug("Found valid CA certificate");
}
}
if (hasTrustedIssuer && clientCert != null) {
x509Credential.setCertificate(clientCert);
return new HandlerResult(this, x509Credential, new SimplePrincipal(x509Credential.getId()));
}
throw new FailedLoginException();
}
public void setTrustedIssuerDnPattern(final String trustedIssuerDnPattern) {
this.regExTrustedIssuerDnPattern = Pattern.compile(trustedIssuerDnPattern);
}
/**
* @param maxPathLength The maxPathLength to set.
*/
public void setMaxPathLength(final int maxPathLength) {
this.maxPathLength = maxPathLength;
}
/**
* @param allowed Allow CA certs to have unlimited intermediate certs (default=false).
*/
public void setMaxPathLengthAllowUnspecified(final boolean allowed) {
this.maxPathLengthAllowUnspecified = allowed;
}
/**
* @param checkKeyUsage The checkKeyUsage to set.
*/
public void setCheckKeyUsage(final boolean checkKeyUsage) {
this.checkKeyUsage = checkKeyUsage;
}
/**
* @param requireKeyUsage The requireKeyUsage to set.
*/
public void setRequireKeyUsage(final boolean requireKeyUsage) {
this.requireKeyUsage = requireKeyUsage;
}
public void setSubjectDnPattern(final String subjectDnPattern) {
this.regExSubjectDnPattern = Pattern.compile(subjectDnPattern);
}
/**
* Sets the component responsible for evaluating certificate revocation status for client
* certificates presented to handler. The default checker is a NO-OP implementation
* for backward compatibility with previous versions that do not perform revocation
* checking.
*
* @param checker Revocation checker component.
*/
public void setRevocationChecker(final RevocationChecker checker) {
this.revocationChecker = checker;
}
private void validate(final X509Certificate cert) throws GeneralSecurityException {
cert.checkValidity();
this.revocationChecker.check(cert);
int pathLength = cert.getBasicConstraints();
if (pathLength < 0) {
if (!isCertificateAllowed(cert)) {
throw new FailedLoginException(
"Certificate subject does not match pattern " + this.regExSubjectDnPattern.pattern());
}
if (this.checkKeyUsage && !isValidKeyUsage(cert)) {
throw new FailedLoginException(
"Certificate keyUsage constraint forbids SSL client authentication.");
}
} else {
// Check pathLength for CA cert
if (pathLength == Integer.MAX_VALUE && this.maxPathLengthAllowUnspecified != true) {
throw new FailedLoginException("Unlimited certificate path length not allowed by configuration.");
} else if (pathLength > this.maxPathLength && pathLength < Integer.MAX_VALUE) {
throw new FailedLoginException(String.format(
"Certificate path length %s exceeds maximum value %s.", pathLength, this.maxPathLength));
}
}
}
private boolean isValidKeyUsage(final X509Certificate certificate) {
logger.debug("Checking certificate keyUsage extension");
/*
* KeyUsage ::= BIT STRING { digitalSignature (0), nonRepudiation (1),
* keyEncipherment (2), dataEncipherment (3), keyAgreement (4),
* keyCertSign (5), cRLSign (6), encipherOnly (7), decipherOnly (8) }
*/
final boolean[] keyUsage = certificate.getKeyUsage();
if (keyUsage == null) {
logger.warn("Configuration specifies checkKeyUsage but keyUsage extension not found in certificate.");
return !this.requireKeyUsage;
}
final boolean valid;
if (isCritical(certificate, KEY_USAGE_OID) || this.requireKeyUsage) {
logger.debug("KeyUsage extension is marked critical or required by configuration.");
valid = keyUsage[0];
} else {
logger.debug(
"KeyUsage digitalSignature=%s, Returning true since keyUsage validation not required by configuration.");
valid = true;
}
return valid;
}
private boolean isCritical(final X509Certificate certificate, final String extensionOid) {
final Set<String> criticalOids = certificate.getCriticalExtensionOIDs();
if (criticalOids == null || criticalOids.isEmpty()) {
return false;
}
return criticalOids.contains(extensionOid);
}
private boolean isCertificateAllowed(final X509Certificate cert) {
return doesNameMatchPattern(cert.getSubjectDN(), this.regExSubjectDnPattern);
}
private boolean isCertificateFromTrustedIssuer(final X509Certificate cert) {
return doesNameMatchPattern(cert.getIssuerDN(), this.regExTrustedIssuerDnPattern);
}
private boolean doesNameMatchPattern(final Principal principal,
final Pattern pattern) {
final String name = principal.getName();
final boolean result = pattern.matcher(name).matches();
logger.debug(String.format("%s matches %s == %s", pattern.pattern(), name, result));
return result;
}
}